<?php

/*

Desktopd Picture Client
Copyright (C) 2015  Desktopd Developer(s)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

https://www.gnu.org/licenses/agpl.html

*/


namespace Desktopd\PictureClient;

use Desktopd\Desktopd\Downloader;
use Desktopd\URIResolver\URIResolver;
use Desktopd\PlatformTools\Loggable;
use Desktopd\PlatformTools\Logger;


class PictureClient implements Loggable, \IteratorAggregate, \Countable {
    protected $downloader;
    protected $pictures = array();
    
    protected $loggers = array();
    
    
    public function __construct ($serverURI, Downloader $downloader = null, $strict = false) {
        if (!($downloader instanceof Downloader)) {
            $downloader = new Downloader; // default configuration
        }
        
        $this->downloader = $downloader;
        
        $json = $downloader->requestResource($serverURI);
        try {
            if (false === $json) {
                throw new \RuntimeException('Request failed');
            }
            
            $json = json_decode($json, JSON_OBJECT_AS_ARRAY);
            if (!is_array($json)) {
                throw new \RuntimeException('Invalid JSON');
            }
            
            $baseURI = new URIResolver($serverURI);
            foreach ($json as $picture) {
                $picture = "$picture";
                if ('' === $picture) {
                    continue;
                }
                
                $this->pictures[] = $baseURI->resolve($picture);
            }
        } catch (\RuntimeException $e) {
            if ($strict) {
                throw $e;
            }
        }
        
    }
    
    public function downloadPictures ($dir, $targetWidth, $targetHeight) {
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        
        if (!is_dir($dir)) {
            $this->log("Could not create directory: $dir", Logger::ERROR);
            return false;
        }
        
        if (!is_writable($dir)) {
            $this->log("Non-writable directory: $dir", Logger::ERROR);
            return false;
        }
        
        $targetWidth = max(1, min(65536, round($targetWidth)));
        $targetHeight = max(1, min(65536, round($targetHeight)));
        $this->log("Target size is: {$targetWidth}x$targetHeight", Logger::INFO);
        
        $downloader = $this->downloader;
        $successCount = 0;
        $skippedCount = 0;
        foreach ($this->pictures as $uri) {
            $filename = hash('sha256', $uri);
            $path = "$dir/$filename";
            $metadataPath = "$path.json";
            if (!file_exists($metadataPath)) {
                preg_match('~^(https?://.+?/[^#?]+)/~', $uri, $matches);
                if (isset($matches[1])) {
                    $metadataURI = "{$matches[1]}/metadata.json";
                    $this->log("Requesting metadata: $metadataURI", Logger::DEBUG);
                    $metadata = $downloader->requestResource($metadataURI);
                    if (false === $metadata) {
                        $this->log("Metadata not available: $metadataURI", Logger::DEBUG);
                    } else {
                        $this->log("Metadata downloaded: $metadataURI", Logger::INFO);
                        if (false === file_put_contents($metadataPath, $metadata)) {
                            $this->log("Could not save metadata to: $metadataPath", Logger::WARNING);
                        }
                    }
                }
            }
            
            if (file_exists($path)) {
                $this->log("Already downloaded: $path", Logger::DEBUG);
                $skippedCount++;
                continue;
            }
            
            preg_match('~^https?://.+?/[^#?]+\.([^#?.]+)~', $uri, $matches);
            $jpeg = false;
            if (isset($matches[1])) {
                $ext = strtolower($matches[1]);
                switch ($ext) {
                    case 'jpg':
                    case 'jpeg':
                    case 'jp2': // JPEG-2000
                        $jpeg = true;
                        break;
                    
                }
            }
            
            $this->log("Downloading: $uri", Logger::INFO);
            $data = $downloader->requestResource($uri);
            if (false === $data) {
                $this->log("Failed to download: $uri", Logger::WARNING);
                $error = $downloader->getLastError();
                $this->log("ERROR: $error", Logger::WARNING);
                continue;
            }
            
            
            $this->log("Downloaded: $uri", Logger::DEBUG);
            
            $tooSmall = false;
            
            /*
                <-- Faster ---------- Slower -->
                    Gmagick    Imagick    GD
                Test:  30s       40s     54s
                
                Note: Internal options are probably different.
            */
            try {
                if (class_exists('Gmagick')) {
                    $magick = 'Gmagick';
                } elseif (class_exists('Imagick')) {
                    $magick = 'Imagick';
                } else {
                    throw new \RuntimeException('No Magick is available');
                }
                
                $this->log("Resizing the image with Gmagick/Imagick", Logger::DEBUG);
                $image = new $magick;
                $image->readImageBlob($data);
                $width = $image->getImageWidth();
                $height = $image->getImageHeight();
                
                $xScale = $width / $targetWidth;
                $yScale = $height / $targetHeight;
                if ($xScale <= 1 || $yScale <= 1) {
                    // resize not needed
                    $image->clear();
                    unset($image);
                    $tooSmall = true;
                    throw new \RuntimeException('Image too small');
                }
                
                unset($data);
                
                $scale = min($xScale, $yScale);
                
                $newWidth = round($width / $scale);
                $newHeight = round($height / $scale);
                $image->resizeImage($newWidth, $newHeight, $magick::FILTER_CUBIC, 1);
                $this->log("Image resized to {$newWidth}x{$newHeight}", Logger::INFO);
                
                file_put_contents($path, $image->getImageBlob());
                /*if (is_callable($image, 'getCompressionQuality')) {
                    var_dump($image->getCompressionQuality());
                } else {
                    $this->log("No such method: getCompressionQuality()", Logger::INFO);
                }*/
                
                $image->clear();
                unset($image);
                
                $successCount++;
                $this->log("Image saved as: $path", Logger::INFO);
                
                continue;
                
            } catch (\RuntimeException $e) {
                $message = $e->getMessage();
                $this->log("$message", Logger::DEBUG);
                // try GD next
            } catch (\Exception $e) {
                $message = $e->getMessage();
                $this->log("Error resizing the image with Gmagick/Imagick ($message)", Logger::ERROR);
                continue;
            }
            
            try {
                if ($tooSmall) {
                    throw new \RuntimeException('Image too small');
                }
                
                if (!function_exists('imagecreatefromstring')) {
                    throw new \RuntimeException('GD is not available');
                }
                
                if (!function_exists('imagetypes')) {
                    throw new \RuntimeException('GD is not available');
                }
                
                if ($jpeg && (imagetypes() & IMG_JPG) == 0) {
                    throw new \RuntimeException('JPEG is not supported by GD');
                }
                
                if ($jpeg == false && (imagetypes() & IMG_PNG) == 0) {
                    throw new \RuntimeException('PNG is not supported by GD');
                }
                
                $this->log("Resizing the image with GD", Logger::DEBUG);
                
                $image = imagecreatefromstring($data);
                if (!is_resource($image)) {
                    throw new \ErrorException('Not supported or invalid image');
                }
                
                //unset($data); // free memory
                
                $width = imagesx($image);
                $height = imagesy($image);
                
                $xScale = $width / $targetWidth;
                $yScale = $height / $targetHeight;
                if ($xScale <= 1 || $yScale <= 1) {
                    // resize not needed
                    imagedestroy($image);
                    throw new \RuntimeException('Image too small');
                }
                
                unset($data); // free memory
                
                $scale = min($xScale, $yScale);
                
                $newWidth = round($width / $scale);
                $newHeight = round($height / $scale);
                $resized = imagecreatetruecolor($newWidth, $newHeight);
                imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
                imagedestroy($image);
                $image = $resized;
                unset($resized);
                
                $this->log("Image resized to {$newWidth}x{$newHeight}", Logger::INFO);
                
                if ($jpeg) {
                    if (!imagejpeg($image, $path/*, 75*/)) {
                        $this->log("Could not save the JPEG image", Logger::ERROR);
                    } else {
                        $successCount++;
                        $this->log("Image saved as: $path", Logger::INFO);
                    }
                } else {
                    if (!imagepng($image, $path, 9)) {
                        $this->log("Could not save the PNG image", Logger::ERROR);
                    } else {
                        $successCount++;
                        $this->log("Image saved as: $path", Logger::INFO);
                    }
                }
                
                imagedestroy($image);
                
            } catch (\Exception $e) {
                $message = $e->getMessage();
                if ($e instanceof \RuntimeException) {
                    $this->log("Not resized ($message)", Logger::DEBUG);
                } else {
                    $this->log("Could not resize the image ($message)", Logger::WARNING);
                }
                file_put_contents($path, $data);
                $this->log("Image saved as: $path", Logger::INFO);
                $successCount++;
            }
            
        }
        
        $this->log("Downloaded $successCount picture(s), skipped $skippedCount", Logger::NOTICE);
        return $successCount;
    }
    
    public function deleteOldFiles ($dir, $maxNumber = 20, $pattern = '^[0-9a-f]{64}$') {
        $maxNumber = max(1, (int) $maxNumber);
        
        $files = glob("$dir/*", GLOB_NOSORT);
        if (!is_array($files)) {
            return false;
        }
        
        $total = count($files);
        if ($total <= $maxNumber) {
            return 0;
        }
        
        $list = new \SplPriorityQueue;
        $total = 0; // is count($list) O(1) ?
        foreach ($files as $file) {
            if (!is_file($file)) {
                continue;
            }
            
            if (!preg_match("\xff$pattern\xff", basename($file))) {
                continue;
            }
            
            $lastModified = filemtime($file);
            if (false !== $lastModified) {
                $list->insert($file, $lastModified);
                $total++;
            } else {
                // ERROR!
                $this->log("Could not get the mtime: $file", Logger::WARNING);
                //$list[] = $file;
            }
        }
        
        unset($files);
        if ($total <= $maxNumber) {
            return 0;
        }
        
        $list->setExtractFlags(\SplPriorityQueue::EXTR_PRIORITY);
        $oldest = PHP_INT_MAX;
        for ($count = 0; $count < $maxNumber; $count++) {
            $oldest = min($oldest, $list->extract());
        }
        
        $list->setExtractFlags(\SplPriorityQueue::EXTR_DATA);
        $deleted = 0;
        foreach ($list as $path) {
            $this->log("Deleting: $path", Logger::DEBUG);
            $jsonPath = "$path.json";
            if (file_exists($jsonPath)) {
                $this->log("Deleting JSON: $jsonPath", Logger::DEBUG);
                unlink($jsonPath);
            }
            unlink($path);
            $deleted++;
        }
        
        $this->log("Deleted: $deleted", Logger::INFO);
        
        $oldest = gmdate('r', $oldest);
        $this->log("$maxNumber files, of which the oldest now: $oldest", Logger::INFO);
        
        return $deleted;
    }
    
    public function getIterator () {
        return new \ArrayIterator($this->pictures);
    }
    
    public function count () {
        return count($this->pictures);
    }
    
    public function hasLogger (Logger $logger) {
        return in_array($logger, $this->loggers, true);
    }
    
    public function addLogger (Logger $logger) {
        if (!in_array($logger, $this->loggers, true)) {
            $this->loggers[] = $logger;
        }
        
        return true;
    }
    
    public function removeLogger (Logger $logger) {
        foreach (array_keys($this->loggers, $logger, true) as $key) {
            unset($this->loggers[$key]);
        }
        
        return true;
    }
    
    protected function log ($message, $level = Logger::INFO) {
        foreach ($this->loggers as $logger) {
            $logger->log($message, $level);
        }
    }
}


// vim: ts=4 et ai

